import dragAndDropRules from './drag-drop-rules.schema.json';

interface DragDropRule {
    canAccept: boolean;
    fixed: boolean;
    hidePadding: boolean;
    [index: string]: boolean;
}

interface Expression {
    target: string;
    operator: string;
    param: any;
    value: any;
}

interface DragAndDropContext {
    children: any;
    parent: any;
    slibing: any;
    [index: string]: any;
}

export type CalculateFunction = (target: string, param: any, value: any, context: DragAndDropContext) => boolean;

export type RuleFunction = (context: DragAndDropContext) => DragDropRule;

export interface UseDragAndDropRule {
    getRuleValue: (componentToken: string, context: DragAndDropContext) => DragDropRule;
}

const ruleMap = new Map<string, any>();

export function useDragAndDropRules(): UseDragAndDropRule {

    function judgingElementCount(target: string, param: any, value: any, context: DragAndDropContext) {
        if (typeof value === 'number') {
            return context[target].length === value;
        }
        if (typeof value === 'object') {
            const compare = Object.keys(value)[0];
            const targetValue = value[compare];
            if (compare === 'not') {
                return Number(context[target].length) !== Number(targetValue);
            }
            if (compare === 'moreThan') {
                return Number(context[target].length) >= Number(targetValue);
            }
            if (compare === 'lessThan') {
                return Number(context[target].length) <= Number(targetValue);
            }
        }
        return false;
    }

    function hasChildren(target: string, param: any, value: any, context: DragAndDropContext) {
        if (typeof value === 'boolean') {
            return context.childrenClassList.includes(param) === Boolean(value);
        }
        return false;
    }

    function hasParent(target: string, param: any, value: any, context: DragAndDropContext) {
        if (typeof value === 'boolean') {
            return context.parentClassList.includes(param) === Boolean(value);
        }
        return false;
    }

    function hasSibling(target: string, param: any, value: any, context: DragAndDropContext) {
        if (typeof value === 'boolean') {
            return context.parentClassList.includes(param) === Boolean(value);
        }
        return false;
    }

    const expressionCalculateFunctions = new Map<string, CalculateFunction>([
        ['length', judgingElementCount],
        ['hasChildren', hasChildren],
        ['hasSibling', hasSibling],
        ['hasParent', hasParent]
    ]);

    function parseExpression(token: string, expression: Record<string, any>): Expression[] {
        const target = token;
        if (typeof expression === 'number') {
            return [{ target, operator: 'length', param: null, value: Number(expression) }];
        }
        if (typeof expression === 'object') {
            return Object.keys(expression).map((key: string) => {
                if (key === 'length') {
                    return { target, operator: 'length', param: null, value: expression[key] };
                }
                const param = key;
                const value = expression[key];
                const operator = token === 'children' ? 'hasChildren' : (token === 'parent' ? 'hasParent' : 'hasSibling');
                return { target, operator, param, value };
            });
        }
        return [];
    }

    function calculateExpression(expression: Expression, context: DragAndDropContext) {
        if (expressionCalculateFunctions.has(expression.operator)) {
            const calculateFunction = expressionCalculateFunctions.get(expression.operator);
            return calculateFunction && calculateFunction(expression.target, expression.param, expression.value, context) || false;
        }
        return false;
    }

    function calculate(expression: Record<string, any>, context: DragAndDropContext): boolean {
        const expressionTokens = Object.keys(expression);

        const parsedExpression = expressionTokens.reduce((expressions: Expression[], token: string) => {
            const result = parseExpression(token, expression[token]);
            expressions.push(...result);
            return expressions;
        }, []);
        const result = parsedExpression.reduce((parsingResult: boolean, expression: Expression) => {
            return parsingResult && calculateExpression(expression, context);
        }, true);

        return result;
    }

    function parseValueSchema(valueSchema: Record<string, any>, context: DragAndDropContext): boolean {
        const schemaKeys = Object.keys(valueSchema);
        const allOf = schemaKeys.includes('allOf');
        const anyOf = schemaKeys.includes('anyOf');
        const hasLogicalOperatorsInSchemaKey = allOf || anyOf;
        const logicalOperator = hasLogicalOperatorsInSchemaKey ? (allOf ? 'allOf' : 'anyOf') : 'allOf';
        const expressions = (hasLogicalOperatorsInSchemaKey ? valueSchema[logicalOperator] : [valueSchema]) as Record<string, any>[];
        const expressionValues = expressions.map((expression: Record<string, any>) => calculate(expression, context));
        const result = allOf ? !expressionValues.includes(false) : expressionValues.includes(true);
        return result;
    }

    function resolveRuleValue(ruleValueSchema: Record<string, any>, context: DragAndDropContext): boolean {
        const valueSchema = ruleValueSchema.const;
        if (!valueSchema) {
            return false;
        }
        if (typeof valueSchema === 'boolean') {
            return valueSchema;
        }
        if (typeof valueSchema === 'object') {
            return parseValueSchema(valueSchema, context);
        }
        return false;
    }

    function generateRuleFunction(rulesSchema: Record<string, any>): RuleFunction {
        return (context: DragAndDropContext) => {
            const rulesInstance = { canAccept: true, fixed: false, hidePadding: false } as DragDropRule;
            rulesSchema && rulesSchema.properties &&
                Object.keys(rulesSchema.properties).reduce((compoentRulesMap: DragDropRule, ruleItemKey: string) => {
                    const ruleItemSchema = rulesSchema.properties[ruleItemKey];
                    compoentRulesMap[ruleItemKey] = resolveRuleValue(ruleItemSchema, context);
                    return compoentRulesMap;
                }, rulesInstance);
            return rulesInstance;
        };
    }

    function resolveComponentRule(componentToken: string, componentSchema: Record<string, any>, ruleMap: Map<string, any>) {
        if (componentSchema.type === 'object' && componentSchema.properties) {
            const { rules: rulesSchema, contents } = componentSchema.properties;
            ruleMap.set(componentToken, generateRuleFunction(rulesSchema));
            if (contents) {
                Object.keys(contents.properties).forEach((subComponentToken: string) =>
                    resolveComponentRule(subComponentToken, contents.properties[subComponentToken], ruleMap)
                );
            }
        }
    }

    function resolveRuleSchema() {
        const { properties: componentsSchema } = dragAndDropRules as Record<string, any>;
        Object.keys(componentsSchema).forEach((componentToken: string) => {
            resolveComponentRule(componentToken, componentsSchema[componentToken], ruleMap);
        });
    }

    function getRuleValue(componentToken: string, context: DragAndDropContext): DragDropRule {
        const rulesInstance = { canAccept: true, fixed: false, hidePadding: true } as DragDropRule;
        if (ruleMap.has(componentToken)) {
            const ruleFuntions = ruleMap.get(componentToken) as RuleFunction;
            return ruleFuntions(context);
        }
        return rulesInstance;
    }

    resolveRuleSchema();

    return { getRuleValue };
}
